﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Jayrock.Json;
using System.IO;
using System.Threading;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;

namespace StackExchangeApi
{
    /// <summary>
    /// Represents a single point on the reputation graph.
    /// </summary>
    public sealed class ReputationChange
    {
        /// <summary>
        /// Title for this change.
        /// Generally, the Title of the Question or Answer
        /// that generated this ReputationChange.
        /// </summary>
        public String Title { get; internal set; }

        /// <summary>
        /// Reputation gain.  This number will be positive or zero.
        /// </summary>
        public long PositiveChange { get; internal set; }

        /// <summary>
        /// Reputation loss.  This number will be negative or zero.
        /// </summary>
        public long NegativeChange { get; internal set; }

        /// <summary>
        /// The Post associated with this ReputationChange.
        /// Could be either a Question or an Answer
        /// </summary>
        public Post Post { get; internal set; }

        /// <summary>
        /// Used for ordering ReputationChanges
        /// when two changes occurred at the same "time"
        /// from the APIs perspective.
        /// </summary>
        internal long Order { get; set; }

        /// <summary>
        /// The earliest this change could have happened
        /// </summary>
        internal DateTime After { get; set; }

        /// <summary>
        /// The latest this change could have happened
        /// </summary>
        internal DateTime Before { get; set; }

        #if DEBUG
        /// <summary>
        /// For debugging purposes only
        /// </summary>
        /// <returns>A string representation of this ReputationChange</returns>
        public override string ToString()
        {
            return Title + " +" + PositiveChange + " -" + NegativeChange;
        }
        #endif
    }

    /// <summary>
    /// Represents a ReputationGraph.
    /// 
    /// Associated with a user.
    /// </summary>
    public sealed class ReputationGraph : ICached
    {
        //The "Trilogy Sites" only let you look at 90 days worth of reputation data at a time.
        //  Thus that's all this API supports
        private static TimeSpan EIGHTY_NINE_DAYS = new TimeSpan(89, 0, 0, 0);

        internal DateTime LowestDate { get; set; }
        internal DateTime HighestDate { get; set; }

        /// <summary>
        /// User associated with this ReputationGraph
        /// </summary>
        public User User { get; internal set; }

        private volatile List<ReputationChange> m_backing_list;

        /// <summary>
        /// Accessor for the list that stores all the ReputationChanges
        /// this ReputationGraph has cached.
        /// </summary>
        internal List<ReputationChange> BackingList { get { return m_backing_list; } set { m_backing_list = value; } }

        /// <summary>
        /// Indexer for reputation.
        /// 
        /// Grabs all reputation changes between the two given dates, in order.
        /// </summary>
        /// <param name="fromDate">Date to start at</param>
        /// <param name="toDate">Date to stop at</param>
        /// <returns>A List with all the ReputationChanges in the range [fromDate, toDate]</returns>
        public ReadOnlyCollection<ReputationChange> this[DateTime fromDate, DateTime toDate]
        {
            get
            {
                DateTime lowerBound, upperBound;

                if(CalculateUpdateRange(this, fromDate, toDate, out lowerBound, out upperBound))
                    UpdateReputationGraph(this, lowerBound, upperBound);

                var q = from ReputationChange change in BackingList where change.After >= fromDate && change.Before <= toDate select change;

                List<ReputationChange> ret = new List<ReputationChange>(q.AsEnumerable());

                //Sort them by the [from, to] request they were received in,
                //  and then by their order in the returned objects
                ret.Sort(new Comparison<ReputationChange>(delegate(ReputationChange a, ReputationChange b)
                {
                    if (a.After > b.Before)
                    {
                        return 1;
                    }

                    if (a.Before < b.After)
                    {
                        return -1;
                    }

                    //Since its an error for there to be overlapping [After, Before] pairs
                    //  we can just assume equality in that regard at this point
                    return a.Order.CompareTo(b.Order);
                }));

                return ret.AsReadOnly();
            }
        }

        /// <summary>
        /// Construct a new ReputationGraph, associating it with the given user
        /// </summary>
        /// <param name="user">User who's reputation will populate this graph.</param>
        internal ReputationGraph(User user)
        {
            User = user;
            BackingList = new List<ReputationChange>();

            //Guarantee that the first request causes a fetch by "folding" the known interval
            LowestDate = DateTime.MaxValue;
            HighestDate = DateTime.MinValue;
        }

        /// <summary>
        /// Calculates the range of updates needed, given the requested range.
        /// </summary>
        /// <param name="graph">The ReputationGraph in question</param>
        /// <param name="proposedFrom">The proposed start</param>
        /// <param name="proposedTo">The proposed from</param>
        /// <param name="from">The actual start of the request, after return</param>
        /// <param name="to">The actual end of the request, after return</param>
        /// <returns>true if an update is needed, and from and to can be used subsequently</returns>
        [MethodImpl(MethodImplOptions.Synchronized)]
        private static bool CalculateUpdateRange(ReputationGraph graph, DateTime proposedFrom, DateTime proposedTo, out DateTime from, out DateTime to)
        {
            //There isn't any data from before the sites launched...
            if (proposedFrom < Utils.GetLaunchDateFor(graph.User.Site))
                proposedFrom = Utils.GetLaunchDateFor(graph.User.Site);

            //Nor will there be any data from the future...
            if (proposedTo > DateTime.Now)
                proposedTo = DateTime.Now;

            //If we don't already have this data locally, go find it
            if (!(proposedFrom >= graph.LowestDate && proposedTo <= graph.HighestDate))
            {
                if (!(proposedFrom >= graph.LowestDate && proposedTo <= graph.HighestDate))
                {
                    //Clip the range so that we ask for as little data as possible
                    DateTime lowerBound = proposedFrom;
                    if (graph.HighestDate > proposedFrom) lowerBound = graph.HighestDate;

                    DateTime upperBound = proposedTo;
                    if (graph.LowestDate < proposedTo) upperBound = graph.LowestDate;

                    from = lowerBound;
                    to = upperBound;

                    return true;
                }
            }

            //Set these, pointless, values
            from = DateTime.MinValue;
            to = DateTime.MinValue;

            return false;
        }

        /// <summary>
        /// Populates the given ReputationGraph with data
        /// between the two dates given
        /// </summary>
        /// <param name="graph">Graph to populate</param>
        /// <param name="from">Time to start at</param>
        /// <param name="to">Time to stop at</param>
        internal static void UpdateReputationGraph(ReputationGraph graph, DateTime from, DateTime to)
        {
            //The S[OUF] interface only allows for 90 days of data,
            //  so we're treating that as a "gentleman's" request limit
            if (to - from > EIGHTY_NINE_DAYS)
            {
                UpdateReputationGraph(graph, from, from + EIGHTY_NINE_DAYS);
                UpdateReputationGraph(graph, from + EIGHTY_NINE_DAYS, to);

                return;
            }

            //This doesn't need to be global, since we're guaranteeing [from,to] uniqueness
            //  Thus order only needs to be tracked within this call
            long order = 0;

            Uri uri = GetReputationUrl(graph.User, from, to);

            String str = ConnectionMediator.MakeRequest(uri);

            JsonArray arr = new JsonArray();
            arr.Import(new JsonTextReader(new StringReader(str)));

            foreach (JsonObject obj in arr)
            {
                String url = (String)obj["Url"];
                String title = (String)obj["Title"];
                long repPos = ((JsonNumber)obj["Gain"]).ToInt64();
                long repNeg = ((JsonNumber)obj["Loss"]).ToInt64();
                bool isQuestion = (bool)obj["IsQ"];

                ReputationChange change = new ReputationChange();
                change.After = from;
                change.Before = to;
                change.Order = order;
                change.Title = title;

                change.PositiveChange = repPos;
                change.NegativeChange = repNeg;

                int p = url.IndexOf('/');
                int q = url.LastIndexOf('#');

                if (isQuestion)
                {
                    change.Post = Question.GetQuestionWithId(Int64.Parse(url, CultureInfo.InvariantCulture), graph.User.Site);
                }
                else
                {
                    String ansUrl = url.Substring(p + 1, q - (p + 1));
                    change.Post = Answer.GetAnswerWithId(Int64.Parse(ansUrl, CultureInfo.InvariantCulture), graph.User.Site);
                }

                order++;
                
                graph.BackingList.Add(change);
            }

            if (from < graph.LowestDate) graph.LowestDate = from;
            if (to > graph.HighestDate) graph.HighestDate = to;
        }

        /// <summary>
        /// Construct a url that, when fetched, grabs a JSON
        /// encoded representation of the user's rep graph.
        /// </summary>
        /// <param name="user">User in question</param>
        /// <param name="from">Time to start at</param>
        /// <param name="to">Time to stop at</param>
        /// <returns>A Uri as above.</returns>
        internal static Uri GetReputationUrl(User user, DateTime from, DateTime to)
        {
            String r = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/users/rep-graph/{1}/{2}/{3}", 
                Utils.GetAddressForSite(user.Site), 
                user.Id, 
                Utils.ToUnixTime(from), 
                Utils.ToUnixTime(to));

            return new Uri(r);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Clear()
        {
            m_backing_list.Clear();
        }
    }
}
